// Copyright (c) 2009 Mike Haight, Andy Kipp
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.


#include "Renderer.h"


using namespace HandBanana::Core;
using namespace HandBanana::Utilities;


Renderer::Renderer(boost::shared_ptr<Window> argWindow) : mWindow(argWindow)
{
    THROW_IF_NULL(argWindow, "argWindow must not be NULL");
}


Renderer::~Renderer(void)
{
    Stop();
}


#ifdef _D3D10_1

//
// Public Methods.
//

void Renderer::BeginDrawing(void)
{
    mSprite->Begin(D3DX10_SPRITE_SORT_TEXTURE);
}


void Renderer::Clear(void)
{
    static const FLOAT clearColor[] = { 0.0, 0.0, 0.0, 1.0 };
    mDevice->ClearRenderTargetView(mRenderTargetView, clearColor);
}


IUnknown* Renderer::CreateTexture(const std::wstring& argTextureFile)
{
    THROW_IF_FALSE(!boost::filesystem::exists(argTextureFile), "The specified file does not exist.");

    ID3D10ShaderResourceView* shaderResourceView = NULL;

    // Create Texture Resource
    {
        ID3D10Resource* resource = NULL;

        THROW_IF_FAILED(
            ::D3DX10CreateTextureFromFileW(
                mDevice,
                argTextureFile.c_str(),
                NULL,
                NULL,
                &resource,
                NULL),
            "D3DX10CreateTextureFromFileW");

        // Get Texture Description
        ID3D10Texture2D* texture = (ID3D10Texture2D*)resource;

        // Create Resource View
        {
            D3D10_TEXTURE2D_DESC desc;
            texture->GetDesc(&desc);

            D3D10_SHADER_RESOURCE_VIEW_DESC SRVDesc;
            ZeroMemory(&SRVDesc, sizeof(SRVDesc));
            SRVDesc.Format = desc.Format;
            SRVDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;
            SRVDesc.TextureCube.MipLevels = desc.MipLevels;
            SRVDesc.TextureCube.MostDetailedMip = 0;

        THROW_IF_FAILED(
            mDevice->CreateShaderResourceView(
                texture,
                &SRVDesc,
                &shaderResourceView),
            "CreateShaderResourceView");

            texture->Release();
        }
    }

    return (IUnknown*)shaderResourceView;
}


void Renderer::Draw(const std::vector<Sprite*>& argSprites)
{
    // Create the sprite pool as a static array, we do not want to heap allocate each time
    static const int SpritePoolSize = 64;
    // sizeof(spritePool) = sizeof(D3DX10_SPRITE) * SpritePoolSize = 104 bytes * 64 = 6.5 kilobytes
    static D3DX10_SPRITE spritePool[SpritePoolSize];

    static D3DXMATRIX matrixScaling;
    static D3DXMATRIX matrixTranslation;

    int poolIndex = 0;
    
    for (std::vector<Sprite*>::const_iterator current = argSprites.begin(); current != argSprites.end(); ++current)
    {
        Sprite* sprite = *current;

        ::D3DXMatrixScaling(
            &matrixScaling,
            2 * sprite->Width / mViewRectangle.Width(),
            2 * sprite->Height / mViewRectangle.Height(),
            1.0f);

        ::D3DXMatrixTranslation(
            &matrixTranslation,
            (2*sprite->X + sprite->Width - 2*mViewRectangle.X()) / mViewRectangle.Width() - 1,
            (-2*sprite->Y - sprite->Height + 2*mViewRectangle.Y()) / mViewRectangle.Height() + 1,
            0.0f);

        static D3DXMATRIX matrix;
        ::D3DXMatrixIdentity(&matrix);

        // Get the sprite coord, size, and texture
        //Frame currentFrame = sprite->Texture->GetFrame(sprite->Frame);
        spritePool[poolIndex].matWorld = matrixScaling * matrixTranslation;
        //spritePool[poolIndex].TexCoord.x = currentFrame.TextureX;
        //spritePool[poolIndex].TexCoord.y = currentFrame.TextureY;
        //spritePool[poolIndex].TexSize.x = currentFrame.TextureWidth;
        //spritePool[poolIndex].TexSize.y = currentFrame.TextureHeight;
        spritePool[poolIndex].ColorModulate = sprite->ColorModulation;
        //spritePool[poolIndex].pTexture = (ID3D10ShaderResourceView*)sprite->Texture->NativeTexturePointer.ToPointer();
        spritePool[poolIndex].TextureIndex = 0;

        // If the poolIndex has become greater than our buffer size, then we
        // must draw the sprites contained therein, and reset the poolIndex to
        // zero.  Note that if the number of sprites in the list is not an even
        // increment of the buffer size, then this does not draw the last chunk
        // of sprites
        if (++poolIndex >= SpritePoolSize)
        {
            // MSDN
            // http://msdn.microsoft.com/en-us/library/bb205604(VS.85).aspx
            //
            // This draw method is most useful when drawing a large number of
            // sprites that have already been sorted on the CPU (or do not need
            // to be sorted), such as in a particle system.
            //
            // We are using DrawSpritesImmediate() and not DrawSpritesBuffered()
            // because we do not want or need the sprites to be sorted in any
            // way.
            mSprite->DrawSpritesImmediate(spritePool, poolIndex, NULL, NULL);
            poolIndex = 0;
        }
    }

    // If there are sprites remaining in the buffer because of an uneven
    // increment, we must draw them out
    if (poolIndex > 0)
    {
        mSprite->DrawSpritesImmediate(spritePool, poolIndex, NULL, NULL);
    }
}


void Renderer::EndDrawing(void)
{
    mSprite->End();
}


void Renderer::Present(void)
{
    mSwapChain->Present(0, 0);
}


void Renderer::SetView(const HandBanana::Core::Rectangle& argRectangle)
{
    mViewRectangle = argRectangle;
}


//
// Private Methods.
//

void Renderer::Start(void)
{
    // Create the D3D10.1 device
    ID3D10BlendState*       blendState = NULL;
    ID3D10Device1*          device = NULL;
    IDXGISwapChain*         dxgiSwapChain = NULL;
    ID3D10RenderTargetView* renderTargetView = NULL;
    ID3DX10Sprite*          sprite = NULL;

    {
        // These are the feature levels to try depending on the feature level of the graphics card
        D3D10_FEATURE_LEVEL1 featureLevels[] =
        {
            D3D10_FEATURE_LEVEL_10_1,
            D3D10_FEATURE_LEVEL_10_0,
            D3D10_FEATURE_LEVEL_9_3,
            D3D10_FEATURE_LEVEL_9_2,
            D3D10_FEATURE_LEVEL_9_1,
        };

        // The swap chain description
        DXGI_SWAP_CHAIN_DESC swapDesc = {0};
        swapDesc.BufferDesc.Width = mWindow->ViewWidth();
        swapDesc.BufferDesc.Height = mWindow->ViewHeight();
        swapDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        swapDesc.SampleDesc.Count = 1;
        swapDesc.SampleDesc.Quality = 0;
        swapDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swapDesc.BufferCount = 1;
        swapDesc.OutputWindow = mWindow->Handle();
        swapDesc.Windowed = TRUE;

        // Device creation flags
        #ifdef _DEBUG
            UINT flags = D3D10_CREATE_DEVICE_DEBUG;
        #else
            UINT flags = 0;
        #endif

        HRESULT hResult;
        for (int i = 0; i < ARRAYSIZE(featureLevels); ++i)
        {
            hResult = ::D3D10CreateDeviceAndSwapChain1(
                NULL,
                D3D10_DRIVER_TYPE_HARDWARE,
                NULL,
                flags,
                featureLevels[i],
                D3D10_1_SDK_VERSION,
                &swapDesc,
                &dxgiSwapChain,
                &device);

            if (SUCCEEDED(hResult))
            {
                //Logger::Info(L"Created D3D10.1 device using driver type {0} and feature level {1}", driverType, featureLevel);
                break;
            }
        }

        // If we ended up failing all of the device creation attempts
        THROW_IF_FAILED(hResult, "D3D10CreateDeviceAndSwapChain1");
    }

    // Get/Set settings on the device
    {
        device->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

        // Set the view port
        {
            D3D10_VIEWPORT viewPort;
            viewPort.Width = mWindow->ViewWidth();
            viewPort.Height = mWindow->ViewHeight();
            viewPort.MinDepth = 0.0f;
            viewPort.MaxDepth = 1.0f;
            viewPort.TopLeftX = 0;
            viewPort.TopLeftY = 0;
            device->RSSetViewports(1, &viewPort);
        }
    }

    // Set the Output Manager blend state
    {
        D3D10_BLEND_DESC blendDescription = {0};

        blendDescription.AlphaToCoverageEnable = FALSE;
        blendDescription.BlendEnable[0] = TRUE;

        // All BlendEnable values must be the same for D3D10_FEATURE_LEVEL_9_*
        if (!(device->GetFeatureLevel() > D3D10_FEATURE_LEVEL_9_3))
        {
            blendDescription.BlendEnable[1] = TRUE;
            blendDescription.BlendEnable[2] = TRUE;
            blendDescription.BlendEnable[3] = TRUE;
            blendDescription.BlendEnable[4] = TRUE;
            blendDescription.BlendEnable[5] = TRUE;
            blendDescription.BlendEnable[6] = TRUE;
            blendDescription.BlendEnable[7] = TRUE;
        }

        blendDescription.SrcBlend = D3D10_BLEND_SRC_ALPHA;
        blendDescription.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;
        blendDescription.SrcBlendAlpha = D3D10_BLEND_SRC_ALPHA;
        blendDescription.DestBlendAlpha = D3D10_BLEND_INV_SRC_ALPHA;
        blendDescription.BlendOp = D3D10_BLEND_OP_ADD;
        blendDescription.BlendOpAlpha = D3D10_BLEND_OP_ADD;
        blendDescription.RenderTargetWriteMask[0] = D3D10_COLOR_WRITE_ENABLE_ALL;

        THROW_IF_FAILED(
            device->CreateBlendState(&blendDescription, &blendState),
            "CreateBlendState");

        device->OMSetBlendState(blendState, NULL, 0xFFFFFFFF);
    }

    // Get the first swapchain buffer for drawing
    {
        ID3D10Texture2D* backBuffer = NULL;

        THROW_IF_FAILED(
            dxgiSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**>(&backBuffer)),
            "GetBuffer");

        // Get the render target
        THROW_IF_FAILED(
            device->CreateRenderTargetView(
                backBuffer,
                NULL,
                &renderTargetView),
            "CreateRenderTargetView");

        device->OMSetRenderTargets(1, &renderTargetView, NULL);

        SAFE_RELEASE(backBuffer);
    }

    // Create a sprite
    {
        THROW_IF_FAILED(
            ::D3DX10CreateSprite(
                device,
                1,
                &sprite),
            "D3DX10CreateSprite");
    }

    // Save the pointers
    {
        mBlendState =       blendState;
        mDevice =           device;
        mSwapChain =        dxgiSwapChain;
        mRenderTargetView = renderTargetView;
        mSprite =           sprite;
    }
}


void Renderer::Stop(void)
{
    if (NULL != mDevice)
    {
        mDevice->ClearState();
    }

    SAFE_RELEASE(mSprite);
    SAFE_RELEASE(mRenderTargetView);
    SAFE_RELEASE(mSwapChain);
    SAFE_RELEASE(mBlendState);
    SAFE_RELEASE(mDevice);
}

/*
void Renderer::WindowResized(Object^ argSender, SizedEventArgs^ e)
{
   // Set the view port
    {
        D3D10_VIEWPORT viewPort;
        viewPort.Width = e->Width;
        viewPort.Height = e->Height;
        viewPort.MinDepth = 0.0f;
        viewPort.MaxDepth = 1.0f;
        viewPort.TopLeftX = 0;
        viewPort.TopLeftY = 0;
        mDevice->RSSetViewports(1, &viewPort);
    }

    // Resize the buffers
    Marshal::ThrowExceptionForHR(
        mSwapChain->ResizeBuffers(
            1,
            e->Width,
            e->Height,
            DXGI_FORMAT_R8G8B8A8_UNORM,
            0));
}
*/
#else
#ifdef _D3D9

//
// Public Methods.
//

void Renderer::BeginDrawing(void)
{
    mDevice->BeginScene();
    mSprite->Begin(NULL);
}


void Renderer::Clear(void)
{
    static const D3DCOLOR clearColor = D3DCOLOR_ARGB(1, 0, 0, 0);
    mDevice->Clear( 0, NULL, D3DCLEAR_TARGET, clearColor, 1.0f, 0 );
}


IUnknown* Renderer::CreateTexture(const std::wstring& argTextureFile)
{
    THROW_IF_FALSE(!boost::filesystem::exists(argTextureFile), "The specified file does not exist.");

    IDirect3DTexture9 *tex = NULL;

    // Create Texture
    {
        THROW_IF_FAILED(
            ::D3DXCreateTextureFromFileEx(
                mDevice,
                argTextureFile.c_str(),
                D3DX_DEFAULT,
                D3DX_DEFAULT,
                D3DX_DEFAULT,
                NULL,
                D3DFMT_UNKNOWN,
                D3DPOOL_MANAGED,
                D3DX_DEFAULT,
                D3DX_DEFAULT,
                D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f),
                NULL,
                NULL,
                &tex),
            "D3DXCreateTextureFromFileEx");
    }

    // Get Texture Description
    D3DSURFACE_DESC desc;
    tex->GetLevelDesc(0, &desc);

    // Return TexturePtr
    return (IUnknown*)tex;
}


void Renderer::Draw(const std::vector<Sprite*>& argSprites)
{
    for (std::vector<Sprite*>::const_iterator current = argSprites.begin(); current != argSprites.end(); ++current)
    {
        Sprite* sprite = *current;

        D3DXMATRIX matrixScaling;
        ::D3DXMatrixScaling(
            &matrixScaling,
            1.0f,
            1.0f,
            0.0f);

        D3DXMATRIX matrixTranslation;
        ::D3DXMatrixTranslation(
            &matrixTranslation,
            (float)(sprite->X - mViewRectangle.X()),
            (float)(sprite->Y - mViewRectangle.Y()),
            0.0f);

        D3DXMATRIX matrixTransform = matrixScaling * matrixTranslation;
        mSprite->SetTransform(&matrixTransform);

        // Get Sprite current Frame
        //int textureHeight = sprite->Texture->Height;
        //int textureWidth = sprite->Texture->Width;
        //Frame currentFrame = sprite->Texture->GetFrame(sprite->Frame);

        RECT textureRect;
        //textureRect.left =      (LONG)(currentFrame.TextureX * textureWidth);
        //textureRect.top =       (LONG)(currentFrame.TextureY * textureHeight);
        //textureRect.bottom =    (LONG)(textureRect.top + (currentFrame.TextureHeight * textureHeight));
        //textureRect.right =     (LONG)(textureRect.left + (currentFrame.TextureWidth * textureWidth));

        mSprite->Draw(
            NULL, //(IDirect3DTexture9*)sprite->Texture->NativeTexturePointer.ToPointer(),
            &textureRect,
            NULL,
            NULL,
            sprite->ColorModulation);

    }
}


void Renderer::EndDrawing(void)
{
    mSprite->End();
    mDevice->EndScene();
}


void Renderer::Present(void)
{
    mDevice->Present(NULL, NULL, NULL, NULL);
}


void Renderer::SetView(const HandBanana::Core::Rectangle& argRectangle)
{
    mViewRectangle = argRectangle;
}


//
// Private Methods.
//

void Renderer::Start(void)
{
    IDirect3D9* d3d9 = NULL;
    IDirect3DDevice9* device = NULL;

    // Create D3D9 Interface
    if (NULL == (d3d9 = Direct3DCreate9(D3D_SDK_VERSION)))
    {
        throw std::exception("Error creating D3D9 Interface.");
    }

    // Get Adapter Display Mode
    D3DDISPLAYMODE d3ddm;
    THROW_IF_FAILED(
        d3d9->GetAdapterDisplayMode(
            D3DADAPTER_DEFAULT,
            &d3ddm),
        "GetAdapterDisplayMode");

    // Check Device Supports Adapter Display Mode
    THROW_IF_FAILED(
        d3d9->CheckDeviceFormat(
            D3DADAPTER_DEFAULT,
            D3DDEVTYPE_HAL,
            d3ddm.Format,
            D3DUSAGE_DEPTHSTENCIL,
            D3DRTYPE_SURFACE,
            D3DFMT_D16),
        "CheckDeviceFormat");

    // Get Adapter Capabilties
    D3DCAPS9 d3dCaps;
    THROW_IF_FAILED(
        d3d9->GetDeviceCaps(
            D3DADAPTER_DEFAULT,
            D3DDEVTYPE_HAL,
            &d3dCaps),
        "GetDeviceCaps");

    // Create D3D9 Device
    {
        D3DPRESENT_PARAMETERS d3dpp;
        ZeroMemory(&d3dpp, sizeof(d3dpp));
        d3dpp.BackBufferFormat       = d3ddm.Format;
        d3dpp.SwapEffect             = D3DSWAPEFFECT_DISCARD;
        d3dpp.Windowed               = TRUE;
        d3dpp.PresentationInterval   = D3DPRESENT_INTERVAL_IMMEDIATE;

        THROW_IF_FAILED(
            d3d9->CreateDevice(
                D3DADAPTER_DEFAULT,
                D3DDEVTYPE_HAL,
                mWindow->Handle(),
                D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                &d3dpp,
                &device),
            "CreateDevice");

        // Setup Alpha Blending
        {
            device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
            device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
            device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
            device->SetTextureStageState(0, D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
        }
    }

    // Create D3D9 Sprite
    ID3DXSprite* sprite = NULL;
    {
        THROW_IF_FAILED(
            ::D3DXCreateSprite(
                device,
                &sprite),
            "D3DXCreateSprite");
    }

    // Create D3D9 Viewport
    {
        D3DVIEWPORT9 viewPort;
        viewPort.X =        0;
        viewPort.Y =        0;
        viewPort.Width =    mWindow->ViewWidth();
        viewPort.Height =   mWindow->ViewHeight();
        viewPort.MinZ =     0.0f;
        viewPort.MaxZ =     1.0f;
        device->SetViewport(&viewPort);
    }

    {
        mInterface = d3d9;
        mDevice = device;
        mSprite = sprite;
    }

    //smLogger->Info("Created D3D9 device");
}


void Renderer::Stop(void)
{
    SAFE_RELEASE(mSprite);
    SAFE_RELEASE(mDevice);
    SAFE_RELEASE(mInterface);
}

/*
void Renderer::WindowResized(Object^ argSender, SizedEventArgs^ e)
{
    // mDevice->Reset();
}
*/

#endif /* _D3D9 */
#endif /* _D3D10_1 */